<?php
namespace Libsignal\protocol;

use Exception;
use Libsignal\IdentityKey;
use Libsignal\util\ByteUtil;
use Libsignal\exceptions\InvalidMessageException;
use Libsignal\exceptions\LegacyMessageException;
use Whispertext\WhisperMessage as Textsecure_WhisperMessage;
use Libsignal\ecc\Curve;

class WhisperMessage extends CiphertextMessage
{
    const MAC_LENGTH = 8;
    protected $messageVersion;
    protected $senderRatchetKey;
    protected $counter;
    protected $previousCounter;
    protected $cipherText;
    protected $serialized;

    /**
     * WhisperMessage constructor.
     * @param string $messageVersion
     * @param string $macKey
     * @param null $senderRatchetKey
     * @param null $counter
     * @param null $previousCounter
     * @param null $cipherText
     * @param null $senderIdentityKey
     * @param null $receiverIdentityKey
     * @param null $serialized
     * @throws InvalidMessageException
     */
    public function __construct($messageVersion = null,
                                    $macKey = null,
                                    $senderRatchetKey = null,
                                    $counter = null,
                                    $previousCounter = null,
                                    $cipherText = null,
                                    $senderIdentityKey = null,
                                    $receiverIdentityKey = null,
                                    $serialized = null)
    {
        if ($serialized == null) {
            $version = ByteUtil::intsToByteHighAndLow($messageVersion, self::CURRENT_VERSION);
            $proto_message = new Textsecure_WhisperMessage();
            $proto_message->setRatchetKey((string) ($senderRatchetKey->serialize()));
            $proto_message->setCounter($counter);
            $proto_message->setPreviousCounter($previousCounter);
            $proto_message->setCiphertext($cipherText);
            $message = $proto_message->serializeToString();

            $mac = $this->getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, ByteUtil::combine([chr((int) $version), $message]));

            $this->serialized = ByteUtil::combine([chr((int) $version), $message, $mac]);
            $this->senderRatchetKey = $senderRatchetKey;
            $this->counter = $counter;
            $this->previousCounter = $previousCounter;
            $this->cipherText = $cipherText;
            $this->messageVersion = $messageVersion;
        } else {
            try {
                $messageParts = ByteUtil::split($serialized, 1, strlen($serialized) - 1 - self::MAC_LENGTH,
                                              self::MAC_LENGTH);

                $version = ord($messageParts[0][0]);
                $message = $messageParts[1];
//                $mac = $messageParts[2];
                if (ByteUtil::highBitsToInt($version) <= self::UNSUPPORTED_VERSION) {
                    throw new LegacyMessageException('Legacy message '.ByteUtil::highBitsToInt($version));
                }
                if (ByteUtil::highBitsToInt($version) > self::CURRENT_VERSION) {
                    throw new InvalidMessageException('Unknown version: '.ByteUtil::highBitsToInt($version));
                }

                $proto_message = new Textsecure_WhisperMessage();
                try {
                    $proto_message->mergeFromString($message);
                } catch (Exception $ex) {
                    throw new InvalidMessageException('Incomplete message.');
                }
                if ($proto_message->getCiphertext() === null || $proto_message->getCounter() === null || $proto_message->getRatchetKey() == null) {
                    throw new InvalidMessageException('Incomplete message.');
                }
                $this->serialized = $serialized;
                $this->senderRatchetKey = Curve::decodePoint($proto_message->getRatchetKey(), 0);
                $this->messageVersion = ByteUtil::highBitsToInt($version);
                $this->counter = $proto_message->getCounter();
                $this->previousCounter = $proto_message->getPreviousCounter();
                $this->cipherText = $proto_message->getCiphertext();
            } catch (Exception $ex) {
                throw new InvalidMessageException($ex->getMessage());
            }
        }
    }

    public function getSenderRatchetKey()
    {
        return $this->senderRatchetKey;
    }

    public function getMessageVersion()
    {
        return $this->messageVersion;
    }

    public function getCounter()
    {
        return $this->counter;
    }

    public function getBody()
    {
        return $this->cipherText;
    }

    public function serialize()
    {
        return $this->serialized;
    }

    public function getType()
    {
        return self::WHISPER_TYPE;
    }

    public function isLegacy($message)
    {
        return $message != null &&  strlen($message) >= 1 && ByteUtil::highBitsToInt($message[0]) <= self::UNSUPPORTED_VERSION;
    }

    /**
     * @param $messageVersion
     * @param $senderIdentityKey
     * @param $receiverIdentityKey
     * @param $macKey
     * @throws InvalidMessageException
     */
    public function verifyMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey)
    {
        $parts = ByteUtil::split($this->serialized, strlen($this->serialized) - self::MAC_LENGTH, self::MAC_LENGTH);
        $ourMac = $this->getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, $parts[0]);
        $theirMac = $parts[1];
        if (strcmp($ourMac, $theirMac) != 0) {
            throw new InvalidMessageException('Bad Mac!');
        }
    }

    /**
     * @param int $messageVersion
     * @param IdentityKey $senderIdentityKey
     * @param IdentityKey $receiverIdentityKey
     * @param $macKey
     * @param $serialized
     * @return bool|string
     */
    private function getMac($messageVersion, $senderIdentityKey, $receiverIdentityKey, $macKey, $serialized)
    {
        $mac = hash_init('sha256', HASH_HMAC, $macKey);
        if ($messageVersion >= 3) {
            hash_update($mac, $senderIdentityKey->getPublicKey()->serialize());
            hash_update($mac, $receiverIdentityKey->getPublicKey()->serialize());
        }
        hash_update($mac, $serialized);
        $result = hash_final($mac, true);

        return ByteUtil::trim($result, self::MAC_LENGTH);
    }
}
